January 05, 2021
해당 포스팅에서는 리액트에서 어떻게 페이지네이션을 직접 구현했는지 정리하겠습니다. 리액트 7과 타입스크립트를 사용했습니다.
페이지네이션에는 두가지 방법이 있습니다.
일단 제가 이 포스팅에서 사용하는 API는 전체 데이터를 지원하지 않고, 전체 데이터의 갯수마저 지원하지 않으므로 2번 방식으로 구현할 수 밖에 없었습니다.
위와 같이 페이지네이션을 통해 페이지별로 다른 데이터를 API로부터 가져오도록 하는 것이 목표입니다.
페이지네이션 바(bar) 를 보여줄 페이지네이션 컴포넌트를 구현합니다.
props로 받아야하는 값들은 아래와 같습니다.
페이지 UI는 현재 페이지 부터 현재 페이지 + 3 페이지 까지 보여주도록 구현했습니다.
그리고, 현재 페이지가 2가 넘어가는 경우, 즉 1이 아닌 경우에는 위와 같이 앞에 가장 첫 페이지인 1페이지를 표시하여 1페이지로 쉽게 돌아갈 수 있도록 했습니다.
양 옆에 있는 ’ < ’ 와 ’ > ’ 페이지는 각각 4페이지 앞으로, 4페이지 뒤로 보내줍니다. 4페이지 앞으로 보내주는 버튼은 현재 페이지가 5 이상이 되어야만 보이도록 구현했습니다.
현재 페이지에 따라서 페이지의 UI가 변경되어야 하므로, 엘리먼트를 담은 페이지 리스트를 State로 관리합니다.
그리고 useEffect를 통해서 현재 페이지에 따라서 페이지 리스트를 변경해주도록 합니다.
const [pages, setPages] = useState<JSX.Element[]>([])
// 현재 페이지에 따라 page list 변경
useEffect(() => {
const temp = []
for (let i = 0; i < 4; i++) {
const pageNumber = props.nowPage + i
temp.push(
<Page
key={pageNumber}
onClick={props.paginate.bind(this, pageNumber)}
nowPage={props.nowPage === pageNumber}
>
{pageNumber}
</Page>
)
}
setPages(temp)
}, [props.nowPage])
아래는 Pagination 컴포넌트 전체 코드입니다. 아이콘은 fontAwsome을 사용했습니다.
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faChevronLeft,
faChevronRight,
} from '@fortawesome/free-solid-svg-icons';
interface Props {
nowPage: number;
paginate: (target: number) => void;
}
const Pagination = (props: Props) => {
const [pages, setPages] = useState<JSX.Element[]>([]);
// 현재 페이지에 따라 page list 변경
useEffect(() => {
const temp = [];
for (let i = 0; i < 4; i++) {
const pageNumber = props.nowPage + i;
temp.push(
<Page
key={pageNumber}
onClick={props.paginate.bind(this, pageNumber)}
nowPage={props.nowPage === pageNumber}>
{pageNumber}
</Page>
);
}
setPages(temp);
}, [props.nowPage]);
return (
<Container>
// nowPage가 5 이상 일경우 < 표시 해줍니다.
{props.nowPage >= 5 && (
<Page onClick={props.paginate.bind(this, props.nowPage - 4)}>
<FontAwesomeIcon icon={faChevronLeft} />
</Page>
)}
// nowPage가 2 이상일 경우 맨 첫 페이지를 표시 해줍니다.
{props.nowPage >= 2 && (
<>
{' '}
<Page onClick={props.paginate.bind(this, 1)}>
1
</Page> ...{' '}
</>
)}
{pages}
<Page onClick={props.paginate.bind(this, props.nowPage + 4)}>
<FontAwesomeIcon icon={faChevronRight} />
</Page>
</Container>
);
};
const Container = styled.div`
width: 100%;
height: 30px;
display: flex;
justify-content: center;
margin: 25px 0;
`;
const Page = styled.div<{ nowPage: boolean }>`
background-color: ${(props) => (props.nowPage ? '#fcba03' : '#fff')};
text-align: center;
padding: 5px 10px;
margin: 0 15px;
cursor: pointer;
`;
export default Pagination;
그러면 위와 같이 준비해놓은 Pagination 컴포넌트를 원하는 Page에서 내 사용해야합니다. 앞으로 이 Page를 Pagination 컴포넌트의 ‘부모 컴포넌트’라고 하겠습니다.
먼저 부모컴포넌트에서 하는 일을 알아봅시다.
부모 컴포넌트에서 현재 페이지를 관리해줘야 하고, fetch 로부터 받아온 데이터를 저장해야 하므로 아래와 같이 State를 정의해주었습니다. ( 여기서 result 컴포넌트의 타입 result는 미리 정의해둔 받아올 데이터의 타입입니다.)
초기 페이지는 1로 정의해주도록 합니다.
const [result, setResult] = useState<result[]>([])
const [page, setPage] = useState<number>(1)
그러면 여기서 API를 fetch 할 타이밍은 어제일까요? 바로 페이지가 변경되었을 때 입니다. 따라서 아래와 같이 page 상태가 변경되었을 때마다 API fetch 함수를 불러와 줍니다.
// page가 변경될 때 fetch를 다시 한다.
useEffect(() => {
fetchAPI()
}, [page])
// API fetch 보내고, 받아온 값 저장하는 함수
const fetchAPI = useCallback(async () => {
const response = await api.getArtistInfo(artist, page.toString())
setResult(response.data.songs)
}, [page, result])
그러면 page는 언제 변경될까요 ? 바로 이전에 만들어놓은 Pagination 컴포넌트에서 onClick 이벤트가 발생할 때 입니다. 따라서 Pagination 컴포넌트에 props로 넣어준 paginate 함수를 아래와 같이 정의합니다.
간단하게 page를 매개변수로 받아온 target으로 변경해줍니다.
// target 으로 page Number를 받아와서 page number를 변경한다.
const paginate = useCallback((target: number) => {
setPage(target)
}, [])
그리고 아래와 같이 렌더링 해줍니다.
return (
<Layout title={'artist'}>
<Container>
{result.length > 0 &&
result.map(data => <Card key={data.id} data={data} />)}
<Pagination nowPage={page} paginate={paginate} />
</Container>
</Layout>
)
api 로부터 마지막 페이지 혹은 전체 데이터 갯수를 받아올 수 없어 아직 완벽한 페이지네이션 구현은 아니지만, 그래도 최대한 고민하면서 구현해보았습니다. 사실, 마지막 페이지가 있더라도 해당 구현 방식으로부터 그렇게 달라지는 점은 많지 않습니다 .ㅎ ㅎ
혹시 잘못된 점이나 궁금한 점이 있으시다면 편하게 댓글 달아주세요~